package it.tukano.swingdeco.controls;

import it.tukano.fun.Function1;
import it.tukano.fun.Function3;
import it.tukano.fun.VFunction3;
import it.tukano.swingdeco.event.MouseAdapterExt;
import it.tukano.swingdeco.event.MouseEventExt;
import java.awt.Component;
import java.awt.Container;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import javax.swing.AbstractButton;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.JTextComponent;

/**
 * Extended popup menu
 * @author pgi
 */
public class JPopupMenuExt extends JPopupMenu {
    
    public static final String
            
            /** Action command for the clear button in the default text popup. Can also
             be used to get the control from the JPopupMenuExt built with the newDefaultTextPopup
             method, with its getButtonForActionCommand method. */
            ACTION_COMMAND_CLEAR = "clear",
            
            /** Action command for the cut button in the default text popup. Can also
             be used to get the control from the JPopupMenuExt built with the newDefaultTextPopup
             method, with its getButtonForActionCommand method.  */
            ACTION_COMMAND_CUT = "cut",
            
            /** Action command for the copy button in the default text popup. Can also
             be used to get the control from the JPopupMenuExt built with the newDefaultTextPopup
             method, with its getButtonForActionCommand method.  */
            ACTION_COMMAND_COPY = "copy",
            
            /** Action command for the paste button in the default text popup. Can also
             be used to get the control from the JPopupMenuExt built with the newDefaultTextPopup
             method, with its getButtonForActionCommand method.  */
            ACTION_COMMAND_PASTE = "paste";

    /* The serial version uid of this class */
    private static final long serialVersionUID = 0;

    /**
     * Creates and returns a new JPopupMenuExt instance with default buttons and
     * behaviors for clearing, cutting, copying and pasting text in a text component
     * @param display the text component that will be targetted by the popup menu
     * @return  a new presetted JPopupMenuExt
     */
    public static JPopupMenuExt newDefaultTextPopup(final JTextComponent display) {
        final JPopupMenuExt popup = new JPopupMenuExt();
        final String[] labels = {"Clear", "Cut", "Copy", "Paste" };
        final Locale defaultLocale = Locale.getDefault();
        if(defaultLocale.getLanguage().equals(Locale.ITALIAN.getLanguage())) {
            labels[0] = "Cancella";
            labels[1] = "Taglia";
            labels[2] = "Copia";
            labels[3] = "Incolla";
        }
        popup.appendMenuItem(labels[0], ACTION_COMMAND_CLEAR);
        popup.appendMenuItem(labels[1], ACTION_COMMAND_CUT);
        popup.appendMenuItem(labels[2], ACTION_COMMAND_COPY);
        popup.appendMenuItem(labels[3], ACTION_COMMAND_PASTE);
        popup.addListener(new VFunction3<Component, Point, AbstractButton>() {

            @Override
            protected void doApply(Component p1, Point p2, AbstractButton p3) {
                String actionCommand = p3.getActionCommand();
                if(ACTION_COMMAND_CLEAR.equals(actionCommand)) {
                    display.setText("");
                } else if(ACTION_COMMAND_COPY.equals(actionCommand)) {
                    display.copy();
                } else if(ACTION_COMMAND_CUT.equals(actionCommand)) {
                    display.cut();
                } else if(ACTION_COMMAND_PASTE.equals(actionCommand)) {
                    display.paste();
                }
            }
        });
        popup.onPopupWillBecomeVisible(new Runnable() {

            public void run() {
                boolean enableClear = display.getDocument().getLength() > 0;
                boolean enableCutAndCopy = display.getSelectionStart() != display.getSelectionEnd();
                boolean enablePaste = Toolkit.getDefaultToolkit().getSystemClipboard().isDataFlavorAvailable(DataFlavor.stringFlavor);
                popup.getButtonForActionCommand("clear").setEnabled(enableClear);
                popup.getButtonForActionCommand("cut").setEnabled(enableCutAndCopy);
                popup.getButtonForActionCommand("copy").setEnabled(enableCutAndCopy);
                popup.getButtonForActionCommand("paste").setEnabled(enablePaste);
            }
        });
        popup.bind(display);
        return popup;
    }

    /** ActionListener used for the controls added with the append method */
    private ActionListener listener = new ActionListener() {

        public void actionPerformed(ActionEvent e) {
            AbstractButton button = (AbstractButton) e.getSource();
            JPopupMenuExt.this.setVisible(false);
            JPopupMenuExt.this.notifyActionPerformed(button);
        }
    };

    /** The location of the mouse when the popup was shown */
    private Point popupLocation = new Point();
    /** The component that fired this menu */
    private Component popupComponent;
    /** The listeners of this popup menu */
    private List<Function3<Component, Point, AbstractButton, ?>> listeners = new LinkedList<Function3<Component, Point, AbstractButton, ?>>();
    /** An optional user object bound to this popup menu ext instance */
    private Object userObject;

    /**
     * Instance initializer
     */
    public JPopupMenuExt() {
    }

    /**
     * Returns the last known popup location
     * @return the popup location
     */
    public Point getLastPopupLocation() {
        return new Point(popupLocation);
    }

    /**
     * Returns the last popup owner
     * @return the last popup owner
     */
    public Component getLastPopupComponent() {
        return popupComponent;
    }

    /**
     * Returns the user object set for this popup (can be null)
     * @return the user object of this popup
     */
    public Object getUserObject() {
        return userObject;
    }

    /**
     * Sets the user object of this popup
     * @param newUserObject the new user object (can be null)
     */
    public void setUserObject(Object newUserObject) {
        userObject = newUserObject;
    }

    /**
     * Add a task executed before this popup will become visible
     * @param task the task to execute
     * @return this
     */
    public JPopupMenuExt onPopupWillBecomeVisible(final Runnable task) {
        this.addPopupMenuListener(new PopupMenuListener() {

            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                task.run();
            }

            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
            }

            public void popupMenuCanceled(PopupMenuEvent e) {
            }
        });
        return this;
    }

    /**
     * Shows this popup menu when a right-mouse click event happens on the given
     * component
     * @param component the component that will popup this menu
     * @return this
     */
    public JPopupMenuExt bind(final Component component) {
        component.addMouseListener(new MouseAdapterExt() {

            @Override
            protected void rightMousePressedExt(MouseEventExt e) {
                popupLocation.setLocation(e.getX(), e.getY());
                popupComponent = e.getComponent();
                JPopupMenuExt.this.show(popupComponent, e.getX(), e.getY());
            }
        });
        return this;
    }

    /**
     * Binds this menu to the given component, using an external function to check if the
     * popup can be triggered or not
     * @param component the target component of this popup menu
     * @param popupConfirm the function that returns true if the popup shoul be triggered or
     * false if the popup should be denied
     * @return this
     */
    public JPopupMenuExt bind(final Component component, final Function1<Point, Boolean> popupConfirm) {
        component.addMouseListener(new MouseAdapterExt() {

            @Override
            protected void rightMousePressedExt(MouseEventExt e) {
                Point popupPoint = e.getPoint();
                if(popupConfirm.apply(popupPoint)) {
                    popupLocation.setLocation(e.getX(), e.getY());
                    popupComponent = e.getComponent();
                    JPopupMenuExt.this.show(popupComponent, e.getX(), e.getY());
                }
            }
        });
        return this;
    }

    /**
     * Add a listeners for the buttons created with this popup menu. The listener
     * will be notified when a button fires an action event. The listener will receive
     * the component that opened this popup menu, the button that fired the event and the
     * location on the component where the popup has been opened
     * @param listener
     * @return this
     */
    public JPopupMenuExt addListener(Function3<Component, Point, AbstractButton, ?> listener) {
        listeners.add(listener);
        return this;
    }

    /**
     * Add a control to this menu.
     * @param buttonLabel the label of the button to add
     * @param actionCommand the action command for the added control, can be null
     * @return this
     */
    public JPopupMenuExt appendMenuItem(String buttonLabel, String actionCommand) {
        JMenuItem button = new JMenuItem(buttonLabel);
        if(actionCommand != null) button.setActionCommand(actionCommand);
        button.addActionListener(listener);
        add(button);
        return this;
    }

    /**
     * Add a control to this menu
     * @param button the control to add
     * @return this
     */
    public JPopupMenuExt appendMenuItem(AbstractButton button) {
        button.addActionListener(listener);
        add(button);
        return this;
    }
    
    /**
     * Add a menu and return this
     * @param menu the menu to add
     * @return this
     */
    public JPopupMenuExt appendMenu(JMenu menu) {
        add(menu);
        return this;
    }

    /**
     * Notifies to the listeners that an action performed event occurred
     * @param button the button that triggered the event
     */
    private void notifyActionPerformed(AbstractButton button) {
        for (Function3<Component, Point, AbstractButton, ?> listener : listeners) {
            listener.apply(popupComponent, popupLocation, button);
        }
    }

    
    /**
     * Set the enabled state for the button with the given action command
     * @param actionCommand the action command of the the button to enalble/disable
     * @param state the new state of the button
     */
    public void setButtonState(String actionCommand, boolean state) {
        ArrayList<Component> components = new ArrayList<Component>();
        components.add(this);
        for (int i= 0; i < components.size(); i++) {
            Component c = components.get(i);
            if(c instanceof AbstractButton) {
                AbstractButton button = (AbstractButton) c;
                if(actionCommand.equals(button.getActionCommand())) {
                    button.setEnabled(state);
                }
            } else if(c instanceof Container) {
                components.addAll(Arrays.asList(((Container)c).getComponents()));
            }
        }
    }
    
    public void setButtonsState(boolean state, String... labels) {
        List<String> list = Arrays.asList(labels);
        for(int i = 0; i < getComponentCount(); i++) {
            Component c = getComponent(i);
            if(c instanceof AbstractButton) {
                AbstractButton b = (AbstractButton) c;
                String text = b.getText();
                if(list.contains(text)) {
                    b.setEnabled(state);
                }
            }
        }
    }
    
    public void disableButtonsLabeled(String... labels) {
        setButtonsState(false, labels);
    }
    
    public void enableButtonsLabeled(String... labels) {
        setButtonsState(true, labels);
    }
    
    public <T extends Component> T findLabeledItem(Class<T> type, String label) {
        Component[] components = getComponents();
        for (int i = 0; i < components.length; i++) {
            Component component = components[i];
            if(component != null && type.isAssignableFrom(component.getClass())) {
                if(component instanceof AbstractButton) {
                    AbstractButton b = (AbstractButton) component;
                    String text = b.getText();
                    if(text.equals(label)) {
                        return type.cast(component);
                    }
                }
            }
        }
        return null;
    }

    public JPopupMenuExt disableMenuLabeled(String label) {
        JMenu item = findLabeledItem(JMenu.class, label);
        if(item != null) {
            item.setEnabled(false);
        }
        return this;
    }
    
    public JPopupMenuExt enableMenuLabeled(String label) {
        JMenu item = findLabeledItem(JMenu.class, label);
        if(item != null) {
            item.setEnabled(true);
        }
        return this;
    }

    public AbstractButton getButtonForActionCommand(String string) {
        Component[] components = getComponents();
        for (int i = 0; i < components.length; i++) {
            Component component = components[i];
            if(component instanceof AbstractButton) {
                AbstractButton b = (AbstractButton) component;
                String text = b.getText();
                if(string.equals(b.getActionCommand())) {
                    return (AbstractButton) component;
                }
            }
        }
        return null;
    }
}
